<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

declare (strict_types = 1); //声明代码的执行指令为开启严格模式,主要为参数类型为严格模式,方便常驻内存

namespace think;

use ReflectionClass;
use ReflectionMethod;


// +----------------------------------------------------------------------
// |
// | (愿景,目的) 名称:事件管理类
// | 功能:
// | (心智)原则:单一职责原则/里氏替换原则/依赖倒置原则/接口隔离原则/迪米特法则/开闭原则
// | 结构组成: 
// | (形式) 类模式: 
// | (模式)模式类型: 观察者模式
// | (模式)设计模式:
// | 产生事件:
// | Understand:Cluster | Cluster Call Butterfly
// |
// | ---------分析--------------
// | 需求的产生 想法
// | 道: <用户-客户立场>|<人的视角>(欲望)
// | 愿景:
// | 目的:
// |
// | =============================
// | 需求的分解--架构-偏商业架构/业务角度
// | 法: <组织内部立场>|<人的视角>
// | 目的:
// | 心智:遵循思维模式,原则
// | (系统)功能:
// | 结构:
// | 协议:
// |
// | =============================
// | 不同环境下的适应性,范围越大越优雅,TP 在复杂应用环境下对领域边界的处理不够,
// | 照成项目文件的复杂错乱,不易迭代,是导致被鄙视的根源
// | 需求点规划/-架构-技术架构,开发上这块是核心 做法
// | 术:<组织内部立场>|<物的视角>
// | 用户/客户立场:(人的视角)(结果)感知
// | 形式:
// |
// | 组织内部立场:(物的视角)(设定)系统思考.逻辑思考
// | 结构:
// |
// | =============================
// | 方案实施 执行 怎么用
// | 器:<物的视角>
// | (转换接口,把物和人联系起来)
// | 形式:
// |
// | (用户/客户立场|视角|(背后是结构)
// | 模式:
// |
// | (具体方法)
// | 事件:
// | -------------------------
// |
// | 知识点:
// | 继承类说明:
// | 点评: 其实我也不怎么明白,事件是怎么回事.这个地方和订阅者是关联的.
// |      这个地方其实是类似钩子标签系统的操作,只是框架把钩子的作用做了分类,这个地方主要用在功能的前后处理上.在不入侵原有代码的情况下加载特定处理
// |      这个处理方式是挺好的,但只是适合做些简单内容的处理,不适合做业务流的转发.不然代码碎片化会严重,导致维护代码无法理解业务的完整,调整不便
// |
// +----------------------------------------------------------------------


/**
 * 事件管理类
 * @package think
 */
class Event
{
    /**
     * 监听者
     * @var array
     */
    protected $listener = [];

    /**
     * 事件别名,其实就是定义别名的调用关系
     * @var array
     */
    protected $bind = [
        'AppInit'     => event\AppInit::class,
        'HttpRun'     => event\HttpRun::class,
        'HttpEnd'     => event\HttpEnd::class,
        'RouteLoaded' => event\RouteLoaded::class,
        'LogWrite'    => event\LogWrite::class,
    ];

    /**
     * 应用对象
     * @var App
     */
    protected $app;

    public function __construct(App $app)
    {
        $this->app = $app;
    }

    /**

     * 批量注册事件监听
     * @access public
     * @param array $events 事件定义
     * @return $this
     */
    public function listenEvents(array $events)
    {

        // 本质也是操作数组,需要在对应的地方,写上事件标签.当程序执行到标签的时候,会进行匹配
        foreach ($events as $event => $listeners) {
            if (isset($this->bind[$event])) {
                $event = $this->bind[$event];
            }

            $this->listener[$event] = array_merge($this->listener[$event] ?? [], $listeners);
        }

        return $this;
    }

    /**
     * 注册事件监听
     * @access public
     * @param string $event    事件名称
     * @param mixed  $listener 监听操作（或者类名）
     * @param bool   $first    是否优先执行
     * @return $this
     */
    public function listen(string $event, $listener, bool $first = false)
    {
        if (isset($this->bind[$event])) {
            $event = $this->bind[$event];
        }

        if ($first && isset($this->listener[$event])) {
            array_unshift($this->listener[$event], $listener);
        } else {
            $this->listener[$event][] = $listener;
        }

        return $this;
    }

    /**
     * 是否存在事件监听
     * @access public
     * @param string $event 事件名称
     * @return bool
     */
    public function hasListener(string $event): bool
    {
        if (isset($this->bind[$event])) {
            $event = $this->bind[$event];
        }

        return isset($this->listener[$event]);
    }

    /**
     * 移除事件监听
     * @access public
     * @param string $event 事件名称
     * @return void
     */
    public function remove(string $event): void
    {
        if (isset($this->bind[$event])) {
            $event = $this->bind[$event];
        }

        unset($this->listener[$event]);
    }

    /**
     * 指定事件别名标识 便于调用
     * @access public
     * @param array $events 事件别名
     * @return $this
     */
    public function bind(array $events)
    {
        $this->bind = array_merge($this->bind, $events);

        return $this;
    }

    /**
     * 注册事件订阅者
     * @access public
     * @param mixed $subscriber 订阅者
     * @return $this
     */
    public function subscribe($subscriber)
    {
        $subscribers = (array) $subscriber;

        foreach ($subscribers as $subscriber) {
            if (is_string($subscriber)) {
                // 创建类的实例
                // 反射调用对应的事件类,这样方便事件订阅者类的编写
                $subscriber = $this->app->make($subscriber);
            }

            // 分析  subscribe 自定义魔术方法是否存在
            if (method_exists($subscriber, 'subscribe')) {
                // 手动订阅
                $subscriber->subscribe($this);
            } else {
                // 智能订阅
                $this->observe($subscriber);
            }
        }

        return $this;
    }

    /**
     * 自动注册事件观察者
     * @access public
     * @param string|object $observer 观察者
     * @param null|string   $prefix   事件名前缀
     * @return $this
     */
    public function observe($observer, string $prefix = '')
    {
        if (is_string($observer)) {
            $observer = $this->app->make($observer);
        }

        $reflect = new ReflectionClass($observer);
        $methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC);

        if (empty($prefix) && $reflect->hasProperty('eventPrefix')) {
            $reflectProperty = $reflect->getProperty('eventPrefix');
            $reflectProperty->setAccessible(true);
            $prefix = $reflectProperty->getValue($observer);
        }

        foreach ($methods as $method) {
            $name = $method->getName();
            // 对 on 的方法进行监听
            if (0 === strpos($name, 'on')) {
                $this->listen($prefix . substr($name, 2), [$observer, $name]);
            }
        }

        return $this;
    }

    /**
     * 触发事件
     * @access public
     * @param string|object $event  事件名称
     * @param mixed         $params 传入参数
     * @param bool          $once   只获取一个有效返回值
     * @return mixed
     */
    public function trigger($event, $params = null, bool $once = false)
    {

        // 本方法需要重点分析
        if (is_object($event)) {
            $params = $event;
            // 返回对象的类名 事件
            $event  = get_class($event);
        }

        // 判断事件别名中是否存在
        if (isset($this->bind[$event])) {
            $event = $this->bind[$event];
        }

        $result    = [];
        // 监听者数组是否存在
        $listeners = $this->listener[$event] ?? [];
        $listeners = array_unique($listeners, SORT_REGULAR);

        foreach ($listeners as $key => $listener) {
            // 执行事件调度,功能实现核心
            $result[$key] = $this->dispatch($listener, $params);

            if (false === $result[$key] || (!is_null($result[$key]) && $once)) {
                break;
            }
        }

        return $once ? end($result) : $result;
    }

    /**
     * 触发事件(只获取一个有效返回值)
     * @param      $event
     * @param null $params
     * @return mixed
     */
    public function until($event, $params = null)
    {
        return $this->trigger($event, $params, true);
    }

    /**
     * 执行事件调度
     * @access protected
     * @param mixed $event  事件方法
     * @param mixed $params 参数
     * @return mixed
     */
    protected function dispatch($event, $params = null)
    {
        // 确定反射调用的对象,函数
        if (!is_string($event)) {
            $call = $event;
        } elseif (strpos($event, '::')) {
            $call = $event;
        } else {
            // 创建类的实例
            $obj  = $this->app->make($event);
            $call = [$obj, 'handle'];
        }

        // 调用反射执行callable 支持参数绑定 真正执行的部分
        return $this->app->invoke($call, [$params]);
    }

}
